package org.krayne.swivel.list;

import java.awt.Component;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Collection;

import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;

/**
 * A {@code CheckboxList} is a {@code JList} that expects
 * {@code CheckboxListItem}s as its elements. You can either add
 * {@code CheckboxListItem}s to a {@code ListModel} or pass in
 * {@code CheckboxListItem}s through one of its constructors.
 * Items that are not {@code CheckboxListItem}s will be rendered without
 * a checkbox.
 * 
 * @author dhsu
 */
public class CheckboxList extends JList {
    private static final long serialVersionUID = 8999789450312095370L;
    
    /**
     * Constructs an empty checkbox list.
     */
    public CheckboxList() {
        this(new Object[] {});
    }
    
    /**
     * Constructs a checkbox list with the specified list data. In order
     * for the items to have checkboxes when rendered, they should be
     * {@code CheckboxListItem}s.
     * 
     * @param <E> {@code CheckboxListItem} in order to be rendered with a
     *            {@code JCheckBox}, otherwise any type
     * @param listData list items.
     */
    public <E> CheckboxList(Collection<E> listData) {
        this(createListModel(listData));
    }
    
    /**
     * Constructs a checkbox list with the specified list data. In order
     * for the items to have checkboxes when rendered, they should be
     * {@code CheckboxListItem}s.
     * 
     * @param <E> {@code CheckboxListItem} in order to be rendered with a
     *            {@code JCheckBox}, otherwise any type
     * @param listData list items.
     */
    public <E> CheckboxList(E[] listData) {
        this(createListModel(listData));
    }
    
    /**
     * Constructs a checkbox list with the specified list model. In order
     * for the list model items to have checkboxes when rendered, they should be
     * {@code CheckboxListItem}s.
     * 
     * @param listModel a list model
     */
    public CheckboxList(ListModel listModel) {
        super(listModel);
        this.setCellRenderer(new CheckboxListCellRenderer());
        this.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        
        // check or uncheck the box when the list item is clicked
        this.addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
               int index = locationToIndex(e.getPoint());
               toggleIsChecked(index);
            }
        });
        
        // check or uncheck the box when enter or space is pressed for an item
        this.addKeyListener(new KeyAdapter() {
            public void keyReleased(KeyEvent e) {
                if(e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_SPACE) {
                    int[] selectedIndices = getSelectedIndices();
                    for(int index : selectedIndices) {
                        toggleIsChecked(index);
                    }
                }
            }
        });
    }
    
    /**
     * Toggles the check state of the list item with the specified index if
     * the item is a {@code CheckboxListItem}.
     * 
     * @param listItemIndex the index of the list item
     */
    private void toggleIsChecked(int listItemIndex) {
        // ensure positive index
        if(listItemIndex < 0) {
            return;
        }
        
        // ensure correct list item type
        Object clickedElement = getModel().getElementAt(listItemIndex);
        if(!(clickedElement instanceof CheckboxListItem)) {
            return;
        }
        
        CheckboxListItem checkboxListItem = (CheckboxListItem) clickedElement;
        checkboxListItem.setSelected(!checkboxListItem.isSelected());
        repaint();
    }
    
    /**
     * Creates a lst model from the specified collection of list items.
     * 
     * @param <E> any type
     * @param listData the list items
     * @return a list model containing the specified list items
     */
    private static <E> ListModel createListModel(Collection<E> listData) {
        DefaultListModel model = new DefaultListModel();
        for(E item : listData) {
            model.addElement(item);
        }
        return model;
    }
    
    /**
     * Creates a lst model from the specified array of list items.
     * 
     * @param <E> any type
     * @param listData the list items
     * @return a list model containing the specified list items
     */
    private static <E> ListModel createListModel(E[] listData) {
        DefaultListModel model = new DefaultListModel();
        for(E item : listData) {
            model.addElement(item);
        }
        return model;
    }
    
    //--------------------------------------------------------------------------
    
    /**
     * A specialized {@code ListCellRenderer} that renders an additional
     * checkbox if the list item is an instance of {@code CheckboxListItem}.
     * If not, a {@code DefaultListCellRenderer} is used.
     */
    private static class CheckboxListCellRenderer implements ListCellRenderer {
        private static final ListCellRenderer DEFAULT_LIST_CELL_RENDERER = new DefaultListCellRenderer();
        
        /**
         * {@inheritDoc}
         */
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            // if we don't have a CheckboxListItem, then just use the default
            // cell renderer
            if(!(value instanceof CheckboxListItem<?>)) {
                return DEFAULT_LIST_CELL_RENDERER.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            }

            CheckboxListItem checkboxListItem = (CheckboxListItem) value;
            if(isSelected) {
                checkboxListItem.setBackground(list.getSelectionBackground());
                checkboxListItem.setForeground(list.getSelectionForeground());
            } else {
                checkboxListItem.setBackground(list.getBackground());
                checkboxListItem.setForeground(list.getForeground());
            }

            checkboxListItem.setEnabled(list.isEnabled());
            checkboxListItem.setFont(list.getFont());
            checkboxListItem.setFocusPainted(false);

            return checkboxListItem;
        }
    }
}
